package org.ddevil.data.set;

import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.ddevil.data.ChangeTracker;
import org.ddevil.data.Data;
import org.ddevil.data.ChangeTracker.Change;
import org.ddevil.data.util.DataSetIterator;
import org.ddevil.data.util.DataUtils;


/**
 * An implementation of KeyedDataSet that wraps another DataSet and maintaines
 * its keys.
 * If the DataSet that is being wrapped is not unique on it's keys as defined
 * by the key IDs passed in, then behavior of this data set is undefined.
 * <p>
 * This Data Set also implements {@link TrackableDataSet} but by default this
 * will be turned off.
 * <p>
 * Note that the implementation {@link cDataSet#importRecord(Data)}
 * is changed to merge the incoming record if there is allready a record with
 * the same key in this DataSet.
 * <p>
 * This is a {@link org.ddevil.data.set.DataSetWrapper} implementation.
 * If the DataSet being wrapped is changed without this class knowing about
 * it then the results will be undefined.
 *
 * @see org.ddevil.data.set.DataSetWrapper
 *
 * @author Rich O'Connell
 * @author Eric Lundin
 */
public class BasicKeyedDataSet<T extends Data> implements KeyedDataSet<T>, TrackableDataSet<T>, DataSetWrapper {

	private static final long serialVersionUID = 2565164152903036080L;

	/** The model backing this DataSet */
    protected DataSet<T> model;

    /** The attribute names that define the keys to this DataSet */
    protected String[] keyIds;

    /** The lookup table for finding row indices based on keys. */
    protected Hashtable<String, Integer> lookup;

    protected boolean trackChanges;

    protected ChangeTracker changeTracker;

    /**
     * Create a new KeyedDataSet that wraps the given data set
     * and uses the given attribute names as keys.
     */
    public BasicKeyedDataSet(DataSet data, String ...keyIDs) {
        this.model = data;
        setKey(keyIDs);
        lookup = new Hashtable<String, Integer>();
        computeLookupTable();
        trackChanges = false;
        changeTracker = new ChangeTracker();
    }

    protected void computeLookupTable(){
        lookup.clear();
        Object[] key;
        for(int i = 0; i < this.model.size(); i++){
            key = new Object[keyIds.length];
            for(int j = 0; j < keyIds.length; j++){
                key[j] = this.model.getDataAt(i, keyIds[j]);
            }
            String hashKey = createHashKey(key);
            if (lookup.containsKey(hashKey)) {
                System.err.println("Developer Warning: Keyed Data Set has non unique records.  " +
                        "Duplicate Key: " + hashKey);
            }
            lookup.put(hashKey, i);
        }
    }

    protected String createHashKey(Object[] key){
        String ret = "";
        for(Object o : key){
            ret += o != null ? o.toString() : "null";
        }
        return ret;
    }

    protected Object[] getKey(Data in){
        Object[] key = new Object[keyIds.length];
        for(int j = 0; j < keyIds.length; j++){
            key[j] = in.getValue(keyIds[j]);
        }
        return key;
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.KeyedDataSet#setKey(java.lang.String...)
     */
    public void setKey(String... dbids) {
        keyIds = new String[dbids.length];
        for(int i = 0; i < dbids.length; i++){
            keyIds[i] = dbids[i];
        }
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.KeyedDataSet#exportRecord(org.ddevil.data.Data)
     */
    public T exportRecord(T keyData) {

        Object[] key = getKey(keyData);

        Integer index = lookup.get(createHashKey(key));
        if(index != null){
            return this.model.exportRecord(index);
        }
        return null;
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#exportRecord(int)
     */
    public T exportRecord(int rowNum) {
        return this.model.exportRecord(rowNum);
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#exportRecords(int...)
     */
    public DataSet<T> exportRecords(int... indices) {
        return this.model.exportRecords(indices);
    }

    /**
     * {@inheritDoc}
     */
    public Object getDataAt(int rowIndex, String dbIdentifier) {
        return this.model.getDataAt(rowIndex, dbIdentifier);
    }
    
    
    public boolean contains(T data) {
    	return lookup.containsKey(createHashKey(getKey(data)));
    }

    /**
     * {@inheritDoc}
     *
     * KeyedDataSet implementation differs from regular DataSet
     */
    public void importRecord(T inData) {

        String key = createHashKey(getKey(inData));
        //if the record already exists then just update it
        if(lookup.containsKey(key)){
            int index = lookup.get(key);

            Data old = null;
            if(trackChanges){
                old = this.model.exportRecord(index);
            }

            for(String id : this.model.getIdentifiers()){
                //only update if we have a new value
                if(inData.contains(id)){
                    this.model.setDataAt(index, id, inData.getValue(id));
                }
            }

            if(trackChanges){
                Data nuu = this.model.exportRecord(index);
                changeTracker.itemModified(old, nuu);
            }
        }else{
            //otherwise add it and reset look up
            this.model.importRecord(inData);
            if(trackChanges){
                changeTracker.itemAdded(inData);
            }
            computeLookupTable();
        }
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#importRecords(org.ddevil.data.DataSet)
     */
    public void importRecords(DataSet<T> inData) {
        for(int i = 0; i < inData.size(); i++){
            this.importRecord(inData.exportRecord(i));
        }
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#setDataAt(int, java.lang.String, java.lang.Object)
     */
    public void setDataAt(int rowIndex, String dbIdentifier, Object data) {
        if(trackChanges){
            Data old = model.exportRecord(rowIndex);
            this.model.setDataAt(rowIndex, dbIdentifier, data);
            Data nuu = model.exportRecord(rowIndex);
            changeTracker.itemModified(old, nuu);
        }else{
            this.model.setDataAt(rowIndex, dbIdentifier, data);
        }

        if(DataUtils.arrayContains(keyIds, dbIdentifier)){
            computeLookupTable();
        }
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#removeRecord(int)
     */
    public void removeRecord(int index) {
        if(trackChanges){
            changeTracker.itemRemoved(this.model.exportRecord(index));
        }
        this.model.removeRecord(index);
        computeLookupTable();
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#clear()
     */
    public void clear() {
        if(trackChanges){
            for(int i = 0; i < this.model.size(); i++){
                changeTracker.itemRemoved(this.model.exportRecord(i));
            }
        }
        this.model.clear();
        computeLookupTable();
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#getIdentifiers()
     */
    public List<String> getIdentifiers() {
        return this.model.getIdentifiers();
    }

    /* (non-Javadoc)
     * @see org.ddevil.data.DataSet#size()
     */
    public int size() {
       return this.model.size();
    }

    /*
     *  (non-Javadoc)
     * @see org.ddevil.data.KeyedDataSet#removeRecord(org.ddevil.data.Data)
     */
    public void removeRecord(T keyData) {
        Object[] key = getKey(keyData);
        Integer index = lookup.get(createHashKey(key));
        removeRecord(index);
    }

    /*
     *  (non-Javadoc)
     * @see org.ddevil.data.KeyedDataSet#replaceRecord(org.ddevil.data.Data)
     */
    public void replaceRecord(T newRecord) {
        String key = createHashKey(getKey(newRecord));
        //if the record allready exists then overwrite it
        if(lookup.containsKey(key)){
            int index = lookup.get(key);

            Data old = null;
            if(trackChanges){
                old = this.model.exportRecord(index);
            }

            for(String id : this.model.getIdentifiers()){
                this.model.setDataAt(index, id, newRecord.getValue(id));
            }

            if(trackChanges){
                Data nuu = this.model.exportRecord(index);
                changeTracker.itemModified(old, nuu);
            }
        }else{
            //otherwise add it and reset look up
            if(trackChanges){
                changeTracker.itemAdded(newRecord);
            }
            this.model.importRecord(newRecord);
            computeLookupTable();
        }

    }

    /*
     *  (non-Javadoc)
     * @see org.ddevil.data.DataSetWrapper#getWrappedDataSet()
     */
    public DataSet getWrappedDataSet() {
        return model;
    }

    /*
     *  (non-Javadoc)
     * @see org.ddevil.data.TrackableDataSet#trackChanges(boolean)
     */
    public void trackChanges(boolean boo) {
        trackChanges = boo;
    }

    /*
     *  (non-Javadoc)
     * @see org.ddevil.data.TrackableDataSet#clearChanges()
     */
    public void clearChanges() {
        changeTracker.clear();
    }

    /*
     *  (non-Javadoc)
     * @see org.ddevil.data.TrackableDataSet#getChanges()
     */
    public List<Change> getChanges() {
        return changeTracker.getChanges();
    }

    /**
     * {@inheritDoc}
     */
	public Iterator<T> iterator() {
		return new DataSetIterator<T>(this);
	}
}
